package com.gc.jbnfgen.gen;

import com.google.common.base.Joiner;
import com.google.common.collect.Iterables;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;

/**
 *
 * @author dmitry.mamonov
 */
public class JBNFRenderer {
    File targetDir;
    String packageNameLiteral;
    Set<JBNF> renderedItems = new HashSet<JBNF>();

    public void render(File baseDir, String[] packageName,JBNF... items){
        targetDir = baseDir;
        packageNameLiteral = Joiner.on(".").join(packageName);
        for(String name:packageName){
            targetDir = new File(targetDir,name);
        }
        if (targetDir.isDirectory()==false){
            if (targetDir.mkdirs()==false){
                throw new IllegalStateException(targetDir.getAbsolutePath());
            }
        }
        for(JBNF item:items){
            renderItem(item);
        }
    }

    private void renderItem(final JBNF item) {
        if (renderedItems.contains(item)==false){
            renderedItems.add(item);
            for(final JBNF nested:item.getNested().values()){
                renderItem(nested);
            }
            if (item instanceof Alternative){
                Alternative alt = (Alternative) item;
                renderAlternative(alt);
            } else if (item instanceof Sequence){
                final Sequence seq = (Sequence) item;
                renderSequence(seq);
            } else if (item instanceof Constant){
                Constant constant = (Constant) item;
                renderConstant(constant);
            } else if (item instanceof Value){
                Value node = (Value) item;
                renderValue(node);
            } else {
                throw new IllegalArgumentException(item.toString());
            }
        }
    }

    private Concat renderPublicClass(String access, String name, String parent, Concat body) {
        Concat src = new Concat();
        src.spaces("package", packageNameLiteral).enter(";");
        src.enter();
        src.glue(renderClass("public "+access,name, parent, body));
        save(name,src);
        return src;
    }

    private Concat renderClass(String access,String name, String parent, Concat body) {
        Concat src = new Concat();
        src.spaces(access, "class", name, "extends", parent, "{").enter();
        src.nested(body).enter();
        src.spaces("}").enter();
        return src;

    }

    private void save(String name, Concat src) {
        System.out.println("save: "+name);
        try {
            PrintWriter out = new PrintWriter(new File(targetDir, name + ".java"));
            out.print(src.toString());
            out.close();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    public Concat renderToStringForField(final String field){
        return new Concat(){{
            spaces("@Override").enter();
            spaces("public","String","toString()","{").enter();
            nested(new Concat(){{
                spaces("return","String.valueOf(").glue(field).spaces(")").enter(";");
            }});
            spaces("}").enter();
        }};
    }

    private void renderSequence(final Sequence seq) {
        renderPublicClass("final",seq.getTypeName(), "Object", new Concat(){{

            for(final Map.Entry<String,JBNF> nested:seq.getNested().entrySet()){
                spaces("private",nested.getValue().getTypeName(),StringUtils.uncapitalize(nested.getKey())).enter(";");
                enter();
            }
            List<String> formalArguments = new ArrayList<String>();
            for(final Map.Entry<String,JBNF> nested:seq.getNested().entrySet()){
                formalArguments.add(nested.getValue().getTypeName()+" "+StringUtils.uncapitalize(nested.getKey()));
            }

            //public constructor.
            spaces("public",seq.getTypeName()).glue("(",Joiner.on(", ").join(formalArguments),") {").enter();
            nested(new Concat(){{
                for(final Map.Entry<String,JBNF> nested:seq.getNested().entrySet()){
                    glue("this",".",StringUtils.uncapitalize(nested.getKey())," ").spaces("=",StringUtils.uncapitalize(nested.getKey())).enter(";");
                }
            }});
            spaces("}").enter();

            //private constuctor for factory.
            spaces("private",seq.getTypeName()).glue("() {").enter();
            spaces("}").enter();

            //factory classes and methods.
            final Map.Entry<String,JBNF> firstEntry = Iterables.get(seq.getNested().entrySet(), 0);
            final Map.Entry<String,JBNF> lastEntry = Iterables.get(seq.getNested().entrySet(), seq.getNested().size()-1);
            final String firstBuilderClassName = StringUtils.capitalize(firstEntry.getKey())+"Builder";
            spaces("public","static",firstBuilderClassName,"builder()","{").enter();
            nested(new Concat(){{
                spaces(seq.getTypeName(),"self","=","new",seq.getTypeName()).glue("()").enter(";");
                spaces("return","self.new",firstBuilderClassName,"()").enter(";");
            }});
            spaces("}").enter();

            int index = 0;
            for(final Map.Entry<String,JBNF> nested:seq.getNested().entrySet()){
                index++;
                final boolean last = seq.getNested().size()==index;
                final String builderClassName = StringUtils.capitalize(nested.getKey())+"Builder";
                final String nextBuilderClassName;
                if (index<seq.getNested().size()){
                    final Map.Entry<String,JBNF> nextEntry = Iterables.get(seq.getNested().entrySet(), index);
                    nextBuilderClassName = StringUtils.capitalize(nextEntry.getKey())+"Builder";
                } else {
                    nextBuilderClassName = seq.getTypeName();
                }
                glue(renderClass("public", builderClassName, "Object", new Concat(){{
                    spaces("private",builderClassName,"()","{}").enter();
                    spaces("public",nextBuilderClassName,StringUtils.uncapitalize(nested.getKey())).glue("(");
                    if (nested.getValue() instanceof Constant==false){
                        spaces(nested.getValue().getTypeName(),StringUtils.uncapitalize(nested.getKey()));
                    }
                    glue(") {").enter();
                    nested(new Concat(){{
                        glue(seq.getTypeName(),".","this",".",StringUtils.uncapitalize(nested.getKey())).spaces(" = ");
                        if (nested.getValue()instanceof Constant==false) {
                            glue(StringUtils.uncapitalize(nested.getKey()));
                        } else {
                            spaces("new",nested.getValue().getTypeName(),"()");
                        }
                        enter(";");
                        if (last){
                            spaces("return","").glue(seq.getTypeName(),".","this").enter(";");
                        } else {
                            spaces("return","").glue(seq.getTypeName(),".","this",".","new"," ").spaces(nextBuilderClassName,"()").enter(";");
                        }
                    }});
                    spaces("}").enter();
                }}));
            }

            //toString method.
            nested(new Concat(){{
                spaces("@Override").enter();
                spaces("public","String","toString()","{").enter();
                nested(new Concat(){{
                    List<String> fieldsToString = new ArrayList<String>();
                    for(final String nested:seq.getNested().keySet()){
                        String nestedField = StringUtils.uncapitalize(nested);
                        fieldsToString.add("String.valueOf("+nestedField+")");
                    }
                    spaces("return",Joiner.on("+\" \"+").join(fieldsToString)).enter(";");
                }});
                spaces("}").enter();
            }});
        }});
    }

    private void renderAlternative(final Alternative alt) {
        renderPublicClass("abstract",alt.getTypeName(), "Object", new Concat(){{
            for(final Map.Entry<String,JBNF> nested:alt.getNested().entrySet()){
                final JBNF nestedValue = nested.getValue();
                final String typeName =alt.getTypeName()+nestedValue.getTypeName();

                //factory method.
                final String factoryMethodName;
                if (nestedValue instanceof Constant){
                    factoryMethodName="create"+StringUtils.capitalize(nested.getKey());
                } else {
                    factoryMethodName="create";
                }
                spaces("public","static",typeName,factoryMethodName,"(");
                if (nestedValue instanceof Constant==false){
                    spaces(nestedValue.getTypeName(),nestedValue.getFieldName());
                }
                spaces(")","{").enter();
                nested(new Concat(){{
                    spaces("return","new",typeName,"(");
                    if (nestedValue instanceof Constant==false){
                        glue(nestedValue.getFieldName());
                    }
                    spaces(")").enter(";");
                }});
                enter("}");

                //nested class (alternative holder).
                nested(renderClass("static",typeName, alt.getTypeName(), new Concat(){{
                    spaces("private",nestedValue.getTypeName(),nestedValue.getFieldName()).enter(";");
                    spaces("public",typeName).glue("(");
                    if (nestedValue instanceof Constant==false){
                        spaces(nestedValue.getTypeName(),nestedValue.getFieldName());
                    }
                    glue(") {").enter();
                    nested(new Concat(){{
                        glue("this",".",nestedValue.getFieldName()," = ");
                        if (nestedValue instanceof Constant){
                            spaces("new",nestedValue.getTypeName(),"()");
                        } else {
                            spaces(nestedValue.getFieldName());
                        }
                        enter(";");
                    }});
                    enter("}");
                    enter();
                    glue(renderToStringForField(nestedValue.getFieldName()));
                }}));
            }
        }});
        for(final JBNF nested:alt.getNested().values()){
            final String typeName = alt.getTypeName()+nested.getTypeName();

        }
    }

    private void renderConstant(final Constant constant) {
        renderPublicClass("final",constant.getTypeName(), "Object", new Concat(){{
            spaces("@Override").enter();
            spaces("public","String","toString()","{").enter();
            nested(new Concat(){{
                spaces("return","").glue('"',StringEscapeUtils.escapeJava(constant.getLitral()),'"').enter(";");
            }});
            spaces("}").enter();
        }});
    }

    private void renderValue(final Value value) {
        renderPublicClass("final", value.getTypeName(), "Object", new Concat(){{
            spaces("private",value.getType().getName(),value.getFieldName()).enter(";");
            spaces("public",value.getTypeName()).glue("(").spaces(value.getType().getName(),value.getFieldName()).glue(") {").enter();
            nested(new Concat(){{
                glue("this",".",value.getFieldName()," ").spaces("=",value.getFieldName()).enter(";");
            }});
            enter("}");
            glue(renderToStringForField(value.getFieldName()));
        }});
    }
}
